package swrsystem.recommend;


import org.jfree.ui.RefineryUtilities;


public class ModifiedSVD {
	
	private static ModifiedSVD instance;

	private int USER = 10;
	private int ITEM = 19;
	private int DIMENSION = 10;
	private int EPOCH = 100;
	private double LEARNINGRATE = 0.001;
	private double LAMDA = 0.02;
	private double ERR_RATIO_THRESHOLD = 0;
	
	private DataObject trainingData;
	private DataObject validationData;
	
//	private double[][] regSqErrReduceRatio = new double[this.DIMENSION][this.EPOCH];
//	private String[] curveLabel = new String[this.DIMENSION]; 
//	private double[][] regSqErrorSeries = new double[this.DIMENSION][this.EPOCH + 1];
	
	private ModifiedSVD() {
		this.init();
		
	}
	
	public static ModifiedSVD getInstance() {
		if(instance == null)
			instance = new ModifiedSVD();
		return instance;
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		ModifiedSVD test = ModifiedSVD.getInstance();
		test.learn();
	}
	
	private void init() {
		// read in configure, including DIMENSION, EPOCH, LEARNINGRATE, LAMDA
		
		// initialize the rating matrix of training data
		// read in USER, ITEM and ratings for the matrix of training data
		this.initData();
		
		System.out.println("original rating matrix of training data:");
		for(int u = 0; u < this.USER; u ++) {
			System.out.printf("u%2d", u);
			for(int i = 0; i < this.ITEM; i ++) {
				System.out.printf("   %3.2f", this.trainingData.getRating(u, i));
			}
			System.out.println("");
		}

		// display calculated average and biases
		System.out.println("\nwith average: " + this.trainingData.getAvgRating() + "\n");
	}
	
	public void learn() {	
		System.out.println("Learning......");
		// learn matrices(vectors) via stochastic gradient descent, biases added
		
		for(int f = 0; f < this.DIMENSION; f ++) {
			int endRound = 0;
			if(this.trainingData.getRegSqErrorSeries(f, 0) == 0) {
				if(f == 0)
					this.trainingData.updateRegSqErrorSeries(f, 0, this.trainingData.regSqError());
				else
					this.trainingData.updateRegSqErrorSeries(f, 0, this.trainingData.getRegSqErrorSeries(f - 1, endRound));
			}
			for (int round = 0; round < this.EPOCH; round++) {
				endRound = round;	// record the ending round
				for (int u = 0; u < this.USER; u++) {
					for (int i = 0; i < this.ITEM; i++) {
						if(this.trainingData.getRating(u, i) > 0) { //ignore pairs without ratings
							// adding biases
							double baseline = this.trainingData.getBaseline(u, i);
							// modify pu and qi via stochastic gradient descent
							double[] pu = this.trainingData.getUserFeature()[u];
							double[] qi = this.trainingData.getItemFeature()[i];
							double eui = this.trainingData.getRating(u, i) - this.trainingData.predictRating(baseline, pu, qi);
							double uv = this.trainingData.getUserFeature(u, f);
							double iv = this.trainingData.getItemFeature(i, f);
							this.trainingData.updateUserFeature(u, f, uv + this.LEARNINGRATE * (eui * iv - this.LAMDA * uv));
							this.trainingData.updateItemFeature(i, f, iv + this.LEARNINGRATE * (eui * uv - this.LAMDA * iv));
						}
					}
				}
				// record the errors and check if error converges
				if(this.converge(f, round))
					break;
//				System.out.println("After round " + (round + 1)
//						+ ", regularized squared error: "
//						+ this.regSqError(rating, userFeature, itemFeature, lamda, avgRating, userAvgBias, itemAvgBias));
			}
		}
		
		// print out the learning result
		System.out.println("\nUser-Feature Matrix:");
		for(int u = 0; u < this.USER; u ++) {
			System.out.printf("u%2d", u);
			for(int f = 0; f < this.DIMENSION; f ++) {
				System.out.printf("   %+-6.4f" ,this.trainingData.getUserFeature(u, f));
			}
			System.out.println("");
		}
		
		System.out.println("\nItem-Feature Matrix:");
		for(int i = 0; i < this.ITEM; i ++) {
			System.out.printf("i%2d", i);
			for(int f = 0; f < this.DIMENSION; f ++) {
				System.out.printf("   %+-6.4f", this.trainingData.getItemFeature(i, f));
			}
			System.out.println("");
		}
		
		System.out.println("\nvalidate:");
		for(int index = 0; index < this.ITEM; index ++) {
			int u = 1;
			double bui = this.trainingData.getBaseline(u, index);
			System.out.printf("user%2d-item%2d: %5.4f\n", u, index, (bui + this.trainingData.dotProduct(this.trainingData.getUserFeature()[u], this.trainingData.getItemFeature()[index])));
		}
		this.validate();
	}
	
	public void validate() {
		final XYChartPlot plot = new XYChartPlot("Feature Learning", "Check Reg Sq Error", 
												 this.trainingData.getCurveLabel(), 
												 this.trainingData.getRegSqErrorSeries(),
												 this.validationData.getCurveLabel(),
												 this.validationData.getRegSqErrorSeries(), 
												 2);
		plot.pack();
		RefineryUtilities.centerFrameOnScreen(plot);
		plot.setVisible(true);
	}

	// temporally generate random rating for test
	private void initData() {
//		double[][] rating = { {5,5,4,5,5,5,4,5,0,0,0,0,0,0,0,0,0,0,0},
//							  {0,0,0,2,3,4,0,5,4,3,5,4,4,5,4,5,4,4,4},
//							  {0,0,0,5,0,5,4,5,0,5,0,0,5,5,3,0,5,0,5},
//							  {4,0,0,0,0,0,0,4,0,3,4,3,5,0,0,0,0,4,0},
//							  {0,0,0,4,0,0,3,5,0,4,4,5,0,0,0,0,4,5,4},
//							  {0,0,0,3,0,5,5,5,0,4,5,5,5,5,0,0,5,0,4},
//							  {5,0,0,0,0,0,0,0,4,0,0,0,0,0,0,5,0,4,4},
//							  {4,0,0,0,4,4,3,3,0,3,0,0,0,5,0,0,0,0,4},
//							  {0,0,2,3,0,5,0,5,0,3,0,4,5,0,0,0,3,5,5},
//							  {4,0,0,0,5,5,0,0,0,0,0,0,0,0,4,4,3,3,0} };
		
		double[][] training_rating = { {5,5,4,5,5,5,4,5,0,0,0,0,0,0,0,0,0,0,0},
				  					   {0,0,0,2,3,4,0,5,4,3,5,4,4,0,4,0,0,0,0},
				  					   {0,0,0,0,0,5,4,5,0,5,0,0,5,5,0,0,5,0,0},
				  					   {4,0,0,0,0,0,0,4,0,3,4,3,5,0,0,0,0,4,0},
				  					   {0,0,0,4,0,0,3,5,0,4,4,5,0,0,0,0,0,0,0},
				  					   {0,0,0,3,0,5,5,5,0,4,5,5,5,5,0,0,0,0,0},
				  					   {5,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0},
				  					   {4,0,0,0,4,4,3,3,0,3,0,0,0,5,0,0,0,0,0},
				  					   {0,0,0,3,0,5,0,5,0,3,0,4,5,0,0,0,0,0,5},
				  					   {0,0,0,0,5,5,0,0,0,0,0,0,0,0,0,4,0,0,0} };
		
		double[][] validation_rating = { {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,4,4,4},
				  						 {0,0,0,5,0,0,0,0,0,0,0,0,0,0,3,0,0,0,5},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,4},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,4},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,4,4},
				  						 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4},
				  						 {0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,3,5,0},
				  						 {4,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,3,3,0} };
		

		this.trainingData = new DataObject(this.LAMDA, this.DIMENSION, this.EPOCH, this.USER, this.ITEM, training_rating);
		this.validationData = new DataObject(this.LAMDA, this.DIMENSION, this.EPOCH, this.USER, this.ITEM, validation_rating);
	}
		
	private boolean converge(int dimension, int round) {
		boolean converge = false;
		this.trainingData.updateCurveLabel(dimension, "tf_" + dimension);
		this.trainingData.updateRegSqErrorSeries(dimension, round + 1, this.trainingData.regSqError());
		this.validationData.updateCurveLabel(dimension, "vf_" + dimension);
		this.validationData.updateRegSqErrorSeries(dimension, round + 1, this.validationData.regSqError());
//		this.regSqErrReduceRatio[dimension][round] = 100000000 * (this.regSqErrorSeries[dimension][round] - this.regSqErrorSeries[dimension][round + 1]) / this.regSqErrorSeries[dimension][round];
		if(this.trainingData.getRegSqErrReduceRatio()[dimension][round] < this.ERR_RATIO_THRESHOLD)
			converge = true;
		return converge;
	}
	

	

}
